﻿/***************************************************COPYRIGHT***************************************
* Copyright (c) 2008, Whiteboard-IT
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that 
* the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following 
 * disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided with the distribution.
*
* Neither the name of Whiteboard-IT, LLC nor the names of its contributors may be used to endorse or promote 
 * products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
*****************************************************************************************************************/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Reflection;
using Mite.Configuration;
using Mite.DSL;
using Rhino.DSL;

namespace Mite
{
	public static class Migrator
	{
		static Migrator()
		{
			_dslFactory = new DslFactory();
			_dslFactory.Register<MigrationBase>(new MigrationDslEngine());
			_dslFactory.BaseDirectory = Environment.CurrentDirectory;
		}

		private static IAdapterFactory AdapterFactory
		{
			get { return IoC.Resolve<IAdapterFactory>(); }
		}

		public static IEnumerable<MigrationMessage> GetWarningsAndErrors(MiteEnvironment environment, int? targetVersion)
		{
			return GetWarningsAndErrors(GetMigrationsFromDirectory(environment));
		}

		public static IEnumerable<MigrationMessage> MigrateTo(MiteEnvironment environment, int targetVersion)
		{
			MigrationBase[] migrationsInOrder
				= GetMigrationsFromDirectory(environment).ToArray();

			return MigrateTo(environment, targetVersion, migrationsInOrder);			
		}

		private static IEnumerable<MigrationBase> GetMigrationsFromDirectory(MiteEnvironment environment)
		{
			return GetMigrationsFromDirectory(environment, DEFAULT_MIGRATIONS_DIRECTORY_PATH);
		}

		private static IEnumerable<MigrationBase> GetMigrationsFromDirectory(MiteEnvironment environment, string directory )
		{
			string[] migrationFiles = Directory.GetFiles(directory, "*.boo",
			                                             SearchOption.TopDirectoryOnly);
			var migrations = new List<MigrationBase>();

			foreach (string file in migrationFiles)
			{
				var migration = _dslFactory.TryCreate<MigrationBase>(file, environment);
				migrations.Add(migration);
			}
			return migrations.OrderBy(x => x.Version);
		}

		public static IEnumerable<MigrationMessage> MigrateToLatest(MiteEnvironment environment,
		                                                                 params Assembly[] assemblies)
		{
			return MigrateTo(environment, null, assemblies);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="assemblies">Assembly which contains at least one class that inherits from Migration base</param>
		/// <param name="targetVersion">Desired version of the schema</param>
		/// <param name="environment">Configuration Settings</param>
		public static IEnumerable<MigrationMessage> MigrateTo(MiteEnvironment environment, int?  targetVersion,
		                                                           params Assembly[] assemblies)
		{
			var migrationTypes = new List<Type>();
			foreach (Assembly assembly in assemblies)
			{
				migrationTypes.AddRange(
					from type in assembly.GetTypes()
					where type.IsSubclassOf(typeof (MigrationBase))
					select type);
			}
			if (migrationTypes.Count  == 0)
				throw new Exception("Non of these assemblies contain types or subtypes of MigrationBase");

			return MigrateTo(environment, targetVersion, migrationTypes.ToArray());
		}

		/// <summary>
		/// Assumes that the Mite files reside in the default location.  ./migrations
		/// </summary>
		/// <param name="environment"></param>
		/// <param name="targetVersion"></param>
		/// <returns></returns>
		public static IEnumerable<MigrationMessage> MigrateTo(MiteEnvironment environment, int? targetVersion)
		{
			return MigrateTo(environment, targetVersion, DEFAULT_MIGRATIONS_DIRECTORY_PATH);
		}


		public static IEnumerable<MigrationMessage> MigrateTo(MiteEnvironment environment, int? targetVersion, string MiteDirectoryPath)
		{
			DirectoryInfo dir = new DirectoryInfo(MiteDirectoryPath);
			if (!dir.Exists)
				throw new DirectoryNotFoundException(string.Format("{0}: Not found", dir.FullName));

			var q = dir.GetFileSystemInfos("*.boo");
			if (q.Count() > 0 )
			{
				return MigrateTo(environment, targetVersion, GetMigrationsFromDirectory(environment, MiteDirectoryPath).ToArray());
			}else if ((q = dir.GetFileSystemInfos("*.dll")).Count() > 0 )
			{
				List<Assembly> assemblies = new List<Assembly>();
				foreach ( var file in q)
					assemblies.Add(Assembly.LoadFile(file.FullName));
				return MigrateTo(environment, targetVersion, assemblies.ToArray());
			}else
			{
				throw new Exception(string.Format("There are no *.dlls or *.boo files in this directory '{0}'", MiteDirectoryPath));
			}
		}

		/// <summary>
		/// Migrates to the version specified.  If validation fails before the migration then it returns the fatal messages
		/// </summary>
		/// <param name="environment"></param>
		/// <param name="targetVersion"></param>
		/// <param name="migrationTypes">Must inherit from MigrationBase</param>
		public static IEnumerable<MigrationMessage> MigrateTo(MiteEnvironment environment, int? targetVersion,
		                                                           params Type[] migrationTypes)
		{
			IEnumerable<MigrationBase> migrations =
				from type in migrationTypes
				let migration =
					(MigrationBase) Activator.CreateInstance(type, IoC.Resolve<IAdapter>(), environment.Database)
				orderby migration.Version
				select migration;

			return MigrateTo(environment, targetVersion, migrations.ToArray());
		}

		/// <summary>
		/// Migrates to the version specified.  If validation fails before the migration then it returns the fatal messages
		/// </summary>
		/// <param name="environment">Mite Environment</param>
		/// <param name="targetVersion">The migration version which the database should be after the migration occurs.</param>
		/// <param name="migrations">The migration classes which you wish to use</param>
		/// <returns></returns>
		public static IEnumerable<MigrationMessage> MigrateTo(MiteEnvironment environment, int? targetVersion,
		                                                           params MigrationBase[] migrations)
		{
			MigrationDirection direction;
			IEnumerable<MigrationBase> migrationsToRun =
				GetMigrationsToRun(environment, targetVersion, out direction, migrations);

			IEnumerable<MigrationMessage> fatalErrors = GetFatalErrors(environment, targetVersion, migrations);
			if (fatalErrors.Count()  > 0)
				return fatalErrors;

			if (direction == MigrationDirection.Up)
				migrationsToRun.OrderBy(migration => migration.Version).ForEach(migration => migration.Run(direction));
			else
			{
				migrationsToRun.OrderByDescending(migration => migration.Version).ForEach(
					migration => migration.Run(direction));
			}
			return null;
		}

		private static IEnumerable<MigrationBase> GetMigrationsToRun(MiteEnvironment environment, int? targetVersion,
		                                                             out MigrationDirection direction,
		                                                             IEnumerable<MigrationBase> migrations)
		{
			IAdapter adapter = AdapterFactory.Create(environment);
			IDbConnection conn = adapter.CreateConnection(environment.Host, environment.Database, environment.Username,
			                                              environment.Password, environment.Options);
			conn.Open();
			Database database = adapter.GetDatabase(conn, environment.Database);
			long currentVersion = adapter.GetSchemaVersion(conn, database);
			direction = MigrationDirection.Up; //Defaults to up
			if (targetVersion.HasValue &&
			    currentVersion == targetVersion.Value)
				return null;
			if (targetVersion.HasValue)
				direction = currentVersion < targetVersion ? MigrationDirection.Up : MigrationDirection.Down;


			// TODO: This is broken as the returned values from CreateSchemaTable are never used.
			if (currentVersion == 0)
				adapter.CreateSchemaTable(conn, database);

			Func<MigrationBase, bool> shouldRun;

			if (direction == MigrationDirection.Up)
			{
				if (targetVersion.HasValue)
				{
					shouldRun =
						migration => migration.Version <= targetVersion.Value && migration.Version > currentVersion;
				}
				else
					shouldRun = migration => migration.Version > currentVersion;
			}
			else
			{
				if (targetVersion.HasValue)
				{
					shouldRun =
						migration => migration.Version > targetVersion.Value && migration.Version <= currentVersion;
				}
				else
					shouldRun = migration => migration.Version <= currentVersion;
			}

			return
				from migration in migrations
				where shouldRun(migration)
				select migration;
		}

		public static IEnumerable<MigrationMessage> GetWarningsAndErrors(MiteEnvironment environment, int? targetVersion,
		                                                                      IEnumerable<MigrationBase> migrations)
		{
			MigrationDirection direction;
			IEnumerable<MigrationBase> migrationsToRun = GetMigrationsToRun(environment, targetVersion, out direction, migrations);
			return GetWarningsAndErrors(migrationsToRun);
		}

		private static IEnumerable<MigrationMessage> GetWarningsAndErrors(IEnumerable<MigrationBase> migrations)
		{
			foreach (MigrationBase mig in migrations)
			{
				foreach (MigrationMessage tm in mig.TransformationMessages)
					yield return tm;
			}
		}

		public static IEnumerable<MigrationMessage> GetFatalErrors(MiteEnvironment environment, int? targetVersion,
		                                                                MigrationBase[] migrations)
		{
			return GetWarningsAndErrors(environment, targetVersion, migrations).Where(mig => mig.IsFatal);
		}

		private const string DEFAULT_MIGRATIONS_DIRECTORY_PATH = ".\\migration";

		private static readonly DslFactory _dslFactory;

		#region Nested type: FileNameStandardsMigrationComparer

		private class FileNameStandardsMigrationComparer : IComparer
		{
			public static FileNameStandardsMigrationComparer Instance
			{
				get { return _instance; }
			}

			#region IComparer Members

			public int Compare(object x, object y)
			{
				int versionA = x.ToString().GetVersion();
				int versionB = x.ToString().GetVersion();

				return versionA.CompareTo(versionB);
			}

			#endregion

			private static readonly FileNameStandardsMigrationComparer _instance =
				new FileNameStandardsMigrationComparer();
		}

		#endregion
	}
}